use std::{net::SocketAddr, sync::Arc};

use anyhow::Ok;
use clap::{value_parser, Arg, ArgMatches, Command};
use opentelemetry::{global, logs::LogError, trace::TraceError, KeyValue};
use opentelemetry_otlp::{ExportConfig, WithExportConfig};
use opentelemetry_sdk::{
    logs::Config, propagation::TraceContextPropagator, runtime, trace, Resource,
};
use sea_orm::Database;
use tower_http::trace::TraceLayer;
use tracing::{level_filters::LevelFilter, Level};
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt};

use crate::{api, settings::Settings, state::ApplicationState};

pub fn configure() -> Command {
    Command::new("serve").about("Start Http server").arg(
        Arg::new("port")
            .short('p')
            .long("port")
            .value_name("PORT")
            .help("Tcp port to listen on")
            .default_value("8080")
            .value_parser(value_parser!(u16)),
    )
}

pub fn handle(matches: &ArgMatches, settings: &Settings) -> anyhow::Result<()> {
    if let Some(matches) = matches.subcommand_matches("serve") {
        let port = *matches.get_one::<u16>("port").unwrap_or(&8080);

        start_tokio(port, settings)?;
    }

    Ok(())
}

fn start_tokio(port: u16, settings: &Settings) -> anyhow::Result<()> {
    tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .build()
        .unwrap()
        .block_on(async move {
            global::set_text_map_propagator(TraceContextPropagator::new());

            let subscriber =
                tracing_subscriber::registry().with(LevelFilter::from_level(Level::DEBUG));

            let telemetry_layer =
                if let Some(otlp_endpoint) = settings.tracing.otlp_endpoint.clone() {
                    let tracer = init_tracer(&otlp_endpoint)?;
                    let _meter_provider = init_metrics(&otlp_endpoint);
                    let _log_provider = init_logs(&otlp_endpoint);
                    Some(tracing_opentelemetry::layer().with_tracer(tracer))
                } else {
                    None
                };

            subscriber
                .with(telemetry_layer)
                .with(fmt::Layer::default())
                .init();

            let db_url = settings.database.url.clone().unwrap();
            let db_conn = Database::connect(db_url)
                .await
                .expect("Database connection failed");

            let state = Arc::new(ApplicationState::new(settings, db_conn)?);
            let addr = SocketAddr::new("127.0.0.1".parse()?, port);

            let router = api::configure(state).layer(TraceLayer::new_for_http());

            tracing::info!("starting axum on port {port}");

            let listener = tokio::net::TcpListener::bind(addr).await?;
            axum::serve(listener, router).await?;

            Ok(())
        })?;

    std::process::exit(0);
}

fn init_tracer(otlp_endpoint: &str) -> Result<opentelemetry_sdk::trace::Tracer, TraceError> {
    opentelemetry_otlp::new_pipeline()
        .tracing()
        .with_exporter(
            opentelemetry_otlp::new_exporter()
                .tonic()
                .with_endpoint(otlp_endpoint),
        )
        .with_trace_config(
            trace::config().with_resource(Resource::new(vec![KeyValue::new(
                "service.name",
                "shelter-project",
            )])),
        )
        .install_batch(runtime::Tokio)
}

fn init_metrics(
    otlp_endpoint: &str,
) -> Result<opentelemetry_sdk::metrics::MeterProvider, opentelemetry::metrics::MetricsError> {
    let export_config = ExportConfig {
        endpoint: otlp_endpoint.to_string(),
        ..ExportConfig::default()
    };
    opentelemetry_otlp::new_pipeline()
        .metrics(runtime::Tokio)
        .with_exporter(
            opentelemetry_otlp::new_exporter()
                .tonic()
                .with_export_config(export_config),
        )
        .with_resource(Resource::new(vec![KeyValue::new(
            opentelemetry_semantic_conventions::resource::SERVICE_NAME,
            "shelter-project",
        )]))
        .build()
}

fn init_logs(otlp_endpoint: &str) -> Result<opentelemetry_sdk::logs::Logger, LogError> {
    opentelemetry_otlp::new_pipeline()
        .logging()
        .with_log_config(
            Config::default().with_resource(Resource::new(vec![KeyValue::new(
                opentelemetry_semantic_conventions::resource::SERVICE_NAME,
                "shelter-project",
            )])),
        )
        .with_exporter(
            opentelemetry_otlp::new_exporter()
                .tonic()
                .with_endpoint(otlp_endpoint.to_string()),
        )
        .install_batch(runtime::Tokio)
}
